package org.jeecg.common.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.Limit;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.RedisUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 接口流量控制切面
 *
 * @author jiangtianyi
 * @version 1.0
 * @date 2023-02-22
 */
@Aspect
@Component
public class LimitAspect {

    /**
     * Redis Key 模板
     */
    private static final String LIMIT_KEY_TEMPLATE = "flow_limit_%s_%s";

    /**
     * 超过限制输出信息模板
     */
    private static final String LIMIT_MESSAGE_TEMPLATE = "超过使用限制，请%d秒后重试";

    private final RedisUtil redisUtil;

    @Pointcut("@annotation(org.jeecg.common.aspect.annotation.Limit)")
    public void pointCut() {
    }

    /**
     * Around切面，记录次数或限制请求
     *
     * @param point {@link ProceedingJoinPoint}
     * @param limit {@link Limit}
     * @return 超过访问限制返回：Result.error("超过使用限制，请xx秒后重试")
     * @since 1.0
     */
    @Around("pointCut() && @annotation(limit)")
    public Object around(ProceedingJoinPoint point, Limit limit) throws Throwable {
        String key = this.getKey();
        Object value = redisUtil.get(key);
        if (value == null) {
            redisUtil.set(key, 1, limit.seconds());
        } else if ((int) value >= limit.value()) {
            String message = String.format(LIMIT_MESSAGE_TEMPLATE, limit.seconds());
            return Result.error(message);
        } else {
            // 未超过限制次数，访问次数+1
            redisUtil.incr(key, 1);
        }
        return point.proceed();
    }

    /**
     * AfterReturning切面，方法未抛出异常并且方法未正确返回结果时相应处理
     *
     * @param limit  {@link Limit}
     * @param object 方法返回结果
     * @since 1.0
     */
    @AfterReturning(value = "pointCut() && @annotation(limit)", returning = "object")
    public void afterReturning(Limit limit, Object object) {
        boolean flag = true;
        if (object instanceof Result<?>) {
            Result<?> result = (Result<?>) object;
            flag = result.isSuccess();
        }
        if (flag && limit.isBasisFailJudgment()) {
            String key = this.getKey();
            if (limit.isRemoveIfSuccess()) {
                redisUtil.del(key);
            } else {
                redisUtil.decr(key, 1);
            }
        }
    }

    /**
     * 生成 Redis Key："flow_limit_" + ip + uri
     *
     * @return redis key
     * @since 1.0
     */
    private String getKey() {
        HttpServletRequest httpServletRequest = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        String ip = IpUtils.getIpAddr(httpServletRequest);
        String uri = httpServletRequest.getRequestURI();
        return String.format(LIMIT_KEY_TEMPLATE, ip, uri);
    }

    public LimitAspect(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

}